解Bug之路 |
您所在的位置:网站首页 › nginx bug › 解Bug之路 |
解Bug之路-Nginx 502 Bad Gateway
前言
事实证明,读过Linux内核源码确实有很大的好处,尤其在处理问题的时刻。当你看到报错的那一瞬间,就能把现象/原因/以及解决方案一股脑的在脑中闪现。甚至一些边边角角的现象都能很快的反应过来是为何。笔者读过一些Linux TCP协议栈的源码,就在解决下面这个问题的时候有一种非常流畅的感觉。 Bug现场首先,这个问题其实并不难解决,但是这个问题引发的现象倒是挺有意思。先描述一下现象吧,
笔者要对自研的dubbo协议隧道网关进行压测(这个网关的设计也挺有意思,准备放到后面的博客里面)。先看下压测的拓扑吧:
去Gateway2的机器上看了一下,没有任何报错。而Gateway1则有大量的502报错。502是Bad Gateway,Nginx的经典报错,首先想到的就是Gateway2不堪重负被Nginx在Upstream中踢掉。
这时候,发现一个有意思的现象,Nginx确用满了CPU!
既然猜测是Nginx的瓶颈,就把Nginx去掉吧。Gateway1和Gateway2直连,压测TPS里面就飙升了,而且Gateway2的CPU最多也就吃了2个核,毫无压力。
由于Nginx机器权限并不在笔者手上,所以一开始没有关注其日志,现在就联系一下对应的运维去看一下吧。在accesslog里面发现了大量的502报错,确实是Nginx的。又看了下错误日志,发现有大量的 Cannot assign requested address由于笔者读过TCP源码,一瞬间就反应过来,是端口号耗尽了!由于Nginx upstream和后端Backend默认是短连接,所以在大量请求流量进来的时候回产生大量TIME_WAIT的连接。
也就是说,只要一分钟之内产生28232(61000-32768)个TIME_WAIT的socket就会造成端口号耗尽,也即470.5TPS(28232/60),只是一个很容易达到的压测值。事实上这个限制是Client端的,Server端没有这样的限制,因为Server端口号只有一个8080这样的有名端口号。而在
upstream中Nginx扮演的就是Client,而Gateway2就扮演的是Nginx
而笔者也很快想明白了Nginx为什么吃满了机器的CPU,问题就出来端口号的搜索过程。
事实上TIME_WAIT过多的原因是其回收时间竟然需要1min,这个1min其实是TCP协议中规定的2MSL时间,而Linux中就固定为1min。 #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */2MSL的原因就是排除网络上还残留的包对新的同样的五元组的Socket产生影响,也就是说在2MSL(1min)之内重用这个五元组会有风险。为了解决这个问题,Linux就采取了一些列措施防止这样的情况,使得在大部分情况下1s之内的TIME_WAIT就可以重用。下面这段代码,就是检测此TIME_WAIT是否重用。 __inet_hash_connect |->__inet_check_established static int __inet_check_established(......) { ...... /* Check TIME-WAIT sockets first. */ sk_nulls_for_each(sk2, node, &head->twchain) { tw = inet_twsk(sk2); // 如果在time_wait中找到一个match的port,就判断是否可重用 if (INET_TW_MATCH(sk2, net, hash, acookie, saddr, daddr, ports, dif)) { if (twsk_unique(sk, sk2, twp)) goto unique; else goto not_unique; } } ...... }而其中的核心函数就是twsk_unique,它的判断逻辑如下: int tcp_twsk_unique(......) { ...... if (tcptw->tw_ts_recent_stamp && (twp == NULL || (sysctl_tcp_tw_reuse && get_seconds() - tcptw->tw_ts_recent_stamp > 1))) { // 对write_seq设置为snd_nxt+65536+2 // 这样能够确保在数据传输速率write_seq = tcptw->tw_snd_nxt + 65535 + 2 ...... return 1; } return 0; }上面这段代码逻辑如下所示:
开启tcp_tw_recyle这个参数会在NAT环境下造成很大的影响,建议不开启。 Nginx upstream改成长连接事实上,上面的一系列问题都是由于Nginx对Backend是短连接导致。 Nginx从 1.1.4 开始,实现了对后端机器的长连接支持功能。在Upstream中这样配置可以开启长连接的功能: upstream backend { server 127.0.0.1:8080; # It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open. The connections parameter should be set to a number small enough to let upstream servers process new incoming connections as well. keepalive 32; keepalive_timeout 30s; # 设置后端连接的最大idle时间为30s }这样前端和后端都是长连接,大家又可以愉快的玩耍了。
由于对单个远端ip:port耗尽会导致CPU吃满这种现象。所以在Nginx在配置Upstream时候需要格外小心。假设一种情况,PE扩容了一台Nginx,为防止有问题,就先配一台Backend看看情况,这时候如果量比较大的话击穿临界点就会造成大量报错(而应用本身确毫无压力,毕竟临界值是470.5TPS(28232/60)),甚至在同Nginx上的非此域名的请求也会因为CPU被耗尽而得不到响应。多配几台Backend/开启tcp_tw_reuse或许是不错的选择。 总结应用再强大也还是承载在内核之上,始终逃不出Linux内核的樊笼。所以对于Linux内核本身参数的调优还是非常有意义的。如果读过一些内核源码,无疑对我们排查线上问题有着很大的助力,同时也能指导我们避过一些坑! 公众号关注笔者公众号,获取更多干货文章:
|
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |